package com.darcytech.debezium.common.network;

import com.darcytech.debezium.common.utils.ObjectMapperUtils;
import com.darcytech.debezium.common.utils.ThreadUtils;

import org.apache.http.Header;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.List;

public class HttpClientWrap {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientWrap.class);

    static final String HTTP_SCHEMA = "http://";

    static final String SLASH = "/";

    static final int MAX_RETRY_TIMES = 2;

    static final long RETRY_SLEEP_MILLS = 150;

    static final long SOCKET_TIMEOUT_SLEEP_MILLS = 5 * 1000;

    static final long HTTP_TIMEOUT_SLEEP_MILLS = 5 * 1000;

    private final CloseableHttpClient httpClient;

    private final AddressManager addressManager;

    public HttpClientWrap(int poolMaxTotal, int poolMaxPerRoute, int connectTimeout,
                          int connectionRequestTimeout, int socketTimeout, List<String> addresses, long failureRetryTime) {
        HttpClientConnectionManager pool = createPool(poolMaxTotal, poolMaxPerRoute);
        httpClient = createHttpClient(pool, connectTimeout, connectionRequestTimeout, socketTimeout);
        addressManager = new AddressManager(addresses, failureRetryTime);
    }

    private HttpClientConnectionManager createPool(int poolMaxTotal, int poolMaxPerRoute) {
        PoolingHttpClientConnectionManager poolConnManager = new PoolingHttpClientConnectionManager();
        poolConnManager.setMaxTotal(poolMaxTotal);
        poolConnManager.setDefaultMaxPerRoute(poolMaxPerRoute);
        return poolConnManager;
    }

    private CloseableHttpClient createHttpClient(HttpClientConnectionManager pool, int connectTimeout, int connectionRequestTimeout, int socketTimeout) {
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(connectTimeout)
                .setConnectionRequestTimeout(connectionRequestTimeout)
                .setSocketTimeout(socketTimeout)
                .build();
        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(pool)
                .setDefaultRequestConfig(config)
                .setRetryHandler(new DefaultHttpRequestRetryHandler(2, false)).build();
        return httpClient;
    }

    public String execute(String uri, String httpMethod, Object params, Header... heads) throws IOException {
        String accessAddress = null;
        String url = null;
        for (int i = 0; ; i++) {
            try {
                accessAddress = addressManager.getAvailableAddress();
                url = HTTP_SCHEMA + accessAddress + SLASH + uri;
                String result = doRequest(url, httpMethod, params, heads);
                addressManager.setOk(accessAddress);
                return result;
            } catch (Throwable cause) {
                if (accessAddress != null) {
                    addressManager.setError(accessAddress);
                }
                if (i < MAX_RETRY_TIMES && addressManager.needWait()) {
                    if (cause instanceof NoHttpResponseException || cause instanceof SocketException) {
                        LOGGER.error("url:{} happen NoHttpResponseException or SocketException", url);
                        ThreadUtils.sleep(RETRY_SLEEP_MILLS);
                    } else if (cause instanceof SocketTimeoutException) {
                        LOGGER.error("url:{} happen SocketTimeoutException", url);
                        ThreadUtils.sleep(SOCKET_TIMEOUT_SLEEP_MILLS);
                    } else if (cause instanceof ConnectTimeoutException) {
                        LOGGER.error("url:{} happen ConnectTimeoutException", url);
                        ThreadUtils.sleep(HTTP_TIMEOUT_SLEEP_MILLS);
                    } else {
                        LOGGER.error("url:{} happen {}", url, cause);
                        throw cause;
                    }
                } else if (i >= MAX_RETRY_TIMES) {
                    LOGGER.error("url:{} happen {}", url, cause);
                    throw cause;
                }
            }

        }
    }

    private String doRequest(String url, String httpMethod, Object params, Header... heads) throws IOException {
        switch (httpMethod) {
            case HttpGet.METHOD_NAME:
                return doGet(url, heads);
            case HttpPost.METHOD_NAME:
                return doPost(url, params, heads);
            case HttpPut.METHOD_NAME:
                return doPut(url, params, heads);
            case HttpDelete.METHOD_NAME:
                doDelete(url, heads);
                return null;
            default:
                throw new IllegalStateException("unknown request type");
        }

    }

    private String doGet(String url, Header... heads) throws IOException {
        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse response = null;
        try {
            if (heads != null) {
                httpGet.setHeaders(heads);
            }
            response = this.httpClient.execute(httpGet);
            String result = EntityUtils.toString(response.getEntity());
            int code = response.getStatusLine().getStatusCode();
            return result;
        } finally {
            try {
                if (response != null)
                    response.close();
            } catch (IOException e) {
                LOGGER.error("response.close() error", e);
            }
        }
    }

    private String doPost(String url, Object params, Header... heads) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        CloseableHttpResponse response = null;
        try {
            StringEntity paramEntity = new StringEntity(ObjectMapperUtils.writeAsString(params));
            paramEntity.setContentEncoding("UTF-8");
            paramEntity.setContentType("application/json");
            httpPost.setEntity(paramEntity);
            if (heads != null) {
                httpPost.setHeaders(heads);
            }
            response = this.httpClient.execute(httpPost);
            return EntityUtils.toString(response.getEntity());
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
                LOGGER.error("response.close() error", e);
            }
        }
    }

    private String doPut(String url, Object params, Header... heads) throws IOException {
        HttpPut httpPut = new HttpPut(url);
        CloseableHttpResponse response = null;
        try {
            StringEntity paramEntity = new StringEntity(ObjectMapperUtils.writeAsString(params));
            paramEntity.setContentEncoding("UTF-8");
            paramEntity.setContentType("application/json");
            httpPut.setEntity(paramEntity);
            if (heads != null) {
                httpPut.setHeaders(heads);
            }
            response = this.httpClient.execute(httpPut);
            return EntityUtils.toString(response.getEntity());
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
                LOGGER.error("response.close() error", e);
            }
        }
    }

    private void doDelete(String url, Header... heads) throws IOException {
        HttpDelete httpDelete = new HttpDelete(url);
        CloseableHttpResponse response = null;
        try {
            if (heads != null) {
                httpDelete.setHeaders(heads);
            }
            response = this.httpClient.execute(httpDelete);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException e) {
                LOGGER.error("response.close() error", e);
            }
        }
    }

}
